package com.raft.kvstore.server;

import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.raft.core.node.Node;
import com.raft.core.node.NodeBuilder;
import com.raft.core.node.NodeEndpoint;
import com.raft.core.node.NodeId;

// TODO load config from file
public class ServerLauncher {

    private static final Logger logger = LoggerFactory.getLogger(ServerLauncher.class);
    private static final String MODE_STANDALONE = "standalone";
    private static final String MODE_STANDBY = "standby";
    private static final String MODE_GROUP_MEMBER = "group-member";

    // TODO why volatile?
    private volatile Server server;

    private void execute(String[] args) throws Exception {
        Options options = new Options();
        // 模式：默认为单机模式
        options.addOption(Option.builder("m")
                .hasArg()
                .argName("mode")
                .desc("start mode, available: standalone, standby, group-member. default is standalone")
                .build());
        // 节点id：必要参数，一般来说，取一个便于区分的名字有助于之后的测试和调试
        options.addOption(Option.builder("i")
                .longOpt("id")
                .hasArg()
                .argName("node-id")
                .required()
                .desc("node id, required. must be unique in group. " +
                        "if starts with mode group-member, please ensure id in group config")
                .build());
        // 主机名：对于集群成员模式来说不需要(因为可以从集群配置中解析出来)，但是在standalone模式和standby模式下需要
        options.addOption(Option.builder("h")
                .hasArg()
                .argName("host")
                .desc("host, required when starts with standalone or standby mode")
                .build());
        // raft服务端口：在集群模式下不需要，因为可以从gc里解析出来
        options.addOption(Option.builder("p1")
                .longOpt("port-raft-node")
                .hasArg()
                .argName("port")
                .type(Number.class)
                .desc("port of raft node, required when starts with standalone or standby mode")
                .build());
        // kv服务端口
        options.addOption(Option.builder("p2")
                .longOpt("port-service")
                .hasArg()
                .argName("port")
                .type(Number.class)
                .required()
                .desc("port of service, required")
                .build());
        // 日志目录：可选参数，如果设置了目录，该参数就必须存在(否则出错)。目录是包含所有日志代的顶层目录，系统启动时会自动选取最新的日志代，每个日志代下面有一个日志条目文件和一个日志条目索引文件。如果不指定日志目录，默认使用基于内存的日志，这样每次日志都会归零
        options.addOption(Option.builder("d")
                .hasArg()
                .argName("data-dir")
                .desc("data directory, optional. must be present")
                .build());
        // 集群配置：默认格式节点id,主机名,端口号 节点id,主机名,端口号...
        options.addOption(Option.builder("gc")
                .hasArgs()
                .argName("node-endpoint")
                .desc("group config, required when starts with group-member mode. format: <node-endpoint> <node-endpoint>..., " +
                        "format of node-endpoint: <node-id>,<host>,<port-raft-node>, eg: A,localhost,8000 B,localhost,8010")
                .build());

        // 命令行例子：-gc A,localhost,2333 B,localhost,2334 C,localhost,2335 -i A -m group-member -p2 2333
        // 如果main方法的参数args长度为0，则输出帮助
        if (args.length == 0) {
            HelpFormatter formatter = new HelpFormatter();
            formatter.printHelp("xraft-kvstore [OPTION]...", options);
            return;
        }

        CommandLineParser parser = new DefaultParser();
        try {
        	// 解析命令行参数，args是main方法的参数
            CommandLine cmdLine = parser.parse(options, args);
            // 默认是standalone模式
            String mode = cmdLine.getOptionValue('m', MODE_STANDALONE);
            switch (mode) {
                case MODE_STANDBY:
                    startAsStandaloneOrStandby(cmdLine, true);
                    break;
                case MODE_STANDALONE:
                    startAsStandaloneOrStandby(cmdLine, false);
                    break;
                case MODE_GROUP_MEMBER:
                    startAsGroupMember(cmdLine);
                    break;
                default:
                    throw new IllegalArgumentException("illegal mode [" + mode + "]");
            }
        } catch (ParseException | IllegalArgumentException e) {
            System.err.println(e.getMessage());
        }
    }

    private void startAsStandaloneOrStandby(CommandLine cmdLine, boolean standby) throws Exception {
        if (!cmdLine.hasOption("p1") || !cmdLine.hasOption("p2")) {
            throw new IllegalArgumentException("port-raft-node or port-service required");
        }

        String id = cmdLine.getOptionValue('i');
        String host = cmdLine.getOptionValue('h', "localhost");
        int portRaftServer = ((Long) cmdLine.getParsedOptionValue("p1")).intValue();
        int portService = ((Long) cmdLine.getParsedOptionValue("p2")).intValue();

        NodeEndpoint nodeEndpoint = new NodeEndpoint(id, host, portRaftServer);
        Node node = new NodeBuilder(nodeEndpoint)
                .setStandby(standby)
                .setDataDir(cmdLine.getOptionValue('d'))
                .build();
        Server server = new Server(node, portService);
        logger.info("start with mode {}, id {}, host {}, port raft node {}, port service {}",
                (standby ? "standby" : "standalone"), id, host, portRaftServer, portService);
        startServer(server);
    }

    /**
     * @param cmdLine -gc A,localhost,2333 B,localhost,2334 C,localhost,2335 -m group-member -i A -p2 3333
     * @throws Exception
     */
    private void startAsGroupMember(CommandLine cmdLine) throws Exception {
    	// 检查gc参数
        if (!cmdLine.hasOption("gc")) {
            throw new IllegalArgumentException("group-config required");
        }

        // 原始集群配置：[A,localhost,2333, B,localhost,2334, C,localhost,2335]
        String[] rawGroupConfig = cmdLine.getOptionValues("gc");
        // 节点id：A
        String rawNodeId = cmdLine.getOptionValue('i');
        // 上层服务的端口：3333
        int portService = ((Long) cmdLine.getParsedOptionValue("p2")).intValue();

        // 解析集群配置
        Set<NodeEndpoint> nodeEndpoints = Stream.of(rawGroupConfig)
                .map(this::parseNodeEndpoint)
                .collect(Collectors.toSet());

        // 组装node组件：[NodeEndpoint{id=B, address=Address{host='localhost', port=2334}},
        // 				NodeEndpoint{id=C, address=Address{host='localhost', port=2335}},
        //				NodeEndpoint{id=A, address=Address{host='localhost', port=2333}}]
        Node node = new NodeBuilder(nodeEndpoints, new NodeId(rawNodeId))
                .setDataDir(cmdLine.getOptionValue('d'))
                .build();
        // 实例化kv服务的server
        Server server = new Server(node, portService);
        logger.info("start as group member, group config {}, id {}, port service {}", nodeEndpoints, rawNodeId, portService);
        // 启动kv服务
        startServer(server);
    }

    private NodeEndpoint parseNodeEndpoint(String rawNodeEndpoint) {
        String[] pieces = rawNodeEndpoint.split(",");
        if (pieces.length != 3) {
            throw new IllegalArgumentException("illegal node endpoint [" + rawNodeEndpoint + "]");
        }
        String nodeId = pieces[0];
        String host = pieces[1];
        int port;
        try {
            port = Integer.parseInt(pieces[2]);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("illegal port in node endpoint [" + rawNodeEndpoint + "]");
        }
        return new NodeEndpoint(nodeId, host, port);
    }

    private void startServer(Server server) throws Exception {
        this.server = server;
        this.server.start();
        Runtime.getRuntime().addShutdownHook(new Thread(this::stopServer, "shutdown"));
    }

    private void stopServer() {
        try {
            server.stop();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        ServerLauncher launcher = new ServerLauncher();
        launcher.execute(args);
    }

}
